-
Notifications
You must be signed in to change notification settings - Fork 1.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Handle notebook conflicts #16352
Conversation
frontend/src/initKea.ts
Outdated
@@ -79,7 +79,7 @@ export function initKea({ routerHistory, routerLocation, beforePlugins }: InitKe | |||
if ( | |||
!ERROR_FILTER_WHITELIST.includes(actionKey) && | |||
(error?.message === 'Failed to fetch' || // Likely CORS headers errors (i.e. request failing without reaching Django) | |||
(error?.status !== undefined && ![200, 201, 204].includes(error.status))) | |||
(error?.status !== undefined && ![200, 201, 204, 409].includes(error.status))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We seem to show the toast on every request. 409 doesn't seem to be used anywhere else and should be handled gracefully so thought it worth adding as a new special case here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmmm I don't know that this wouldn't cause issues elsewhere...
A more localized way would be to simply catch the error from the API call in the logic and check for the error status.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That would mean the little toast appears in the bottom right hand corner which seemed uncessary given the error is handled in the UI
The other option would be to add the saveNotebook
action to the ERROR_FILTER_WHITELIST
array. That has the effect of not showing the toast for any failed response from the server for that action which felt less good e.g. an actual 500 would not show anything in the UI despite having failed to save
posthog/frontend/src/initKea.ts
Lines 12 to 16 in 0bb4898
/* | |
Actions for which we don't want to show error alerts, | |
mostly to avoid user confusion. | |
*/ | |
const ERROR_FILTER_WHITELIST = [ |
What we really want is a combination of the saveNotebook
and 409 response
. It doesn't look like there is any way to inject conditions given it is a global listener 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually...
Maybe I can implement something within the logic. It seems to get passed as an argument to the onFailure
callback
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please ignore... covered in #16352 (comment)
c9d0506
to
9055be3
Compare
@@ -82,10 +89,7 @@ export const notebookLogic = kea<notebookLogicType>([ | |||
throw new Error('Notebook not found') | |||
} | |||
|
|||
if (!values.notebook) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We only load notebooks when the component mounts so I thought it was ok to remove this check as it was preventing the refreshed content from being populated into the editor
instance.last_modified_by = self.context["request"].user | ||
with transaction.atomic(): | ||
# Lock the database row so we ensure version updates are atomic | ||
locked_instance = Notebook.objects.select_for_update().get(pk=instance.pk) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not super familiar with Django but it seemed necessary to me that the instance needed to be loaded with select_for_update
to lock the table row as part of the transaction. Is there a cleaner way to override the instance
lookup within the serializer so maybe I don't need to load it a second time?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know is my answer here :D
A quick google showed me that you can mark an entire view as atomic with a decorator . No idea if that would work here but worth a try? That way the entire call is in a transaction?
Not sure how that plays into what you say about needing to use select_for_update
though...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Checked it out and the decorator didn't break anything but I also couldn't see an easy way of testing and handling the error....
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like that Notebook.objects.select_for_update()
is super explicit though. makes me happy we're not having to rely on some dark magic
frontend/src/initKea.ts
Outdated
@@ -79,7 +79,7 @@ export function initKea({ routerHistory, routerLocation, beforePlugins }: InitKe | |||
if ( | |||
!ERROR_FILTER_WHITELIST.includes(actionKey) && | |||
(error?.message === 'Failed to fetch' || // Likely CORS headers errors (i.e. request failing without reaching Django) | |||
(error?.status !== undefined && ![200, 201, 204].includes(error.status))) | |||
(error?.status !== undefined && ![200, 201, 204, 409].includes(error.status))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmmm I don't know that this wouldn't cause issues elsewhere...
A more localized way would be to simply catch the error from the API call in the logic and check for the error status.
saveNotebookFailure: ({ errorObject }) => { | ||
if (errorObject.code === 'conflict') { | ||
actions.showConflictWarning() | ||
} | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should instead do this in the saveNotebook
call. Partly because there we can better control the error state, as well as reset the notebook
to a null value or something which would stop it from flashing the old content.
Locally this loads really fast but in prod it will definitely be slower so the old version will likely still be visible for a second or so which might be confusing...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ahh gotcha. Please ignore my comment above. This seems like a better approach
instance.last_modified_by = self.context["request"].user | ||
with transaction.atomic(): | ||
# Lock the database row so we ensure version updates are atomic | ||
locked_instance = Notebook.objects.select_for_update().get(pk=instance.pk) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know is my answer here :D
A quick google showed me that you can mark an entire view as atomic with a decorator . No idea if that would work here but worth a try? That way the entire call is in a transaction?
Not sure how that plays into what you say about needing to use select_for_update
though...
@@ -33,6 +33,11 @@ def __init__(self, feature: Optional[str] = None) -> None: | |||
) | |||
|
|||
|
|||
class Conflict(APIException): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Crazy that there is no built in for this in Django 😅
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
May be because we're a major version behind 🙈
📸 UI snapshots have been updated1 snapshot changes in total. 0 added, 1 modified, 0 deleted:
Triggered by this commit. |
f6674e2
to
d55888a
Compare
📸 UI snapshots have been updated1 snapshot changes in total. 0 added, 1 modified, 0 deleted:
Triggered by this commit. |
SELECT "posthog_notebook"."id", | ||
"posthog_notebook"."short_id", | ||
"posthog_notebook"."team_id", | ||
"posthog_notebook"."title", | ||
"posthog_notebook"."content", | ||
"posthog_notebook"."deleted", | ||
"posthog_notebook"."version", | ||
"posthog_notebook"."created_at", | ||
"posthog_notebook"."created_by_id", | ||
"posthog_notebook"."last_modified_at", | ||
"posthog_notebook"."last_modified_by_id" | ||
FROM "posthog_notebook" | ||
WHERE "posthog_notebook"."id" = '01891c84-73f6-0000-8ea4-5d223f446585'::uuid | ||
LIMIT 21 | ||
FOR | ||
UPDATE /*controller='project_notebooks-detail',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/notebooks/%28%3FP%3Cshort_id%3E%5B%5E/.%5D%2B%29/%3F%24'*/ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cool the snapshot captured the fir update, yay
So, if someone breaks it in future, this will highlight it to them
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
looks good to me, tried locally and it does what it says on the tin
Would be cool to move past this and on to conflict resolution but that's for another day 🙈
ecd8c2d
to
149bfdb
Compare
Problem
Towards #15680
Notebooks do not support multiplayer editing so instead we are going to render a conflict error if the content has been updated elsewhere since the last save
Changes
409
errorHow did you test this code?
Manually (gif included)